﻿using Microsoft.Office.InfoPath;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography.Xml;
using System.Text;
using System.Xml;
using System.Xml.XPath;

namespace InfoPathHelpers.Cryptography
{
    /// <summary>
    /// Provides tools for encrypting entire forms using standard XML encryption.
    /// Note: This requires a very specific structure for the MainDataSource of the form. 
    /// For further information please refer to the documentation.
    /// </summary>
    public class XmlEncryption
    {
        private const string ENCRYPTED_DATA_XPATH = "//enc:EncryptedData";

        private FormContext _FormContext;
        private XmlDocument _Document;
        private EncryptedXml _Encrypter;
        private string _UnencryptedDataXPath;

        /// <summary>
        /// Creates a new instance of the XmlEncryption class using the supplied FormContext and XPath.
        /// </summary>
        /// <param name="formContext">A form context instance referencing the form to encrypt.</param>
        /// <param name="unencryptedDataXPath">The XPath of the "UnencryptedData" element.</param>
        public XmlEncryption(FormContext formContext, string unencryptedDataXPath)
        {
            _UnencryptedDataXPath = unencryptedDataXPath;
            _FormContext = formContext;
            _Document = new XmlDocument();
            _Encrypter = new EncryptedXml(_Document);
        }

        /// <summary>
        /// Uses the supplied certificate to encrypt the data in the "UnencryptedData" element.
        /// </summary>
        /// <param name="certificate">The certificate to use for encryption.</param>
        public void EncryptWithCertificate(X509Certificate2 certificate)
        {
            LoadDocument();
            var UnencryptedElements = _Document.SelectNodes(_UnencryptedDataXPath,
                (XmlNamespaceManager)_FormContext.NamespaceManager) as XmlNodeList;
            foreach (XmlElement UnencryptedElement in UnencryptedElements)
            {
                var EncryptedElement = _Encrypter.Encrypt(UnencryptedElement, certificate);
                EncryptedXml.ReplaceElement(UnencryptedElement, EncryptedElement, false);
            }
            ReplaceXml();
        }

        /// <summary>
        /// Uses the supplied certificate to decrypt the document.
        /// </summary>
        /// <param name="certificate">The certificate to use for decryption.</param>
        public void DecryptWithCertificate()
        {
            LoadDocument();
            _Encrypter.DecryptDocument();

            var NamespaceManager = (XmlNamespaceManager)_FormContext.NamespaceManager;

            List<XPathNavigator> EncryptedElements = new List<XPathNavigator>();

            foreach (XPathNavigator EncryptedElementNode in _FormContext.Select(ENCRYPTED_DATA_XPATH))
            {
                EncryptedElements.Add(EncryptedElementNode);
            }

            foreach(XPathNavigator EncryptedElement in EncryptedElements)
            {
                EncryptedElement.MoveToParent();
                var XPath = InfoPathHelpers.Xml.XmlHelpers.GetFullXPath(EncryptedElement) + "/*[1]";
                var DecryptedData = _Document.SelectSingleNode(XPath, NamespaceManager);
                EncryptedElement.InnerXml = DecryptedData.OuterXml;
            }
        }

        private void ReplaceXml()
        {
            var FormElement = _FormContext.SelectSingleNode("/*");
            var DocumentElement = _Document.CreateNavigator().SelectSingleNode("/*");
            FormElement.InnerXml = DocumentElement.InnerXml;
        }

        private void LoadDocument()
        {
            // The document needs to be re-loaded to pickup changes.
            _Document.LoadXml(_FormContext.Root.OuterXml);
        }
    }
}
